﻿/* crypto-js进行AES加密,安装: npm i --save crypto-js
 * jsencrypt进行RSA加密,安装: npm i --save jsencrypt
 * 官网:https://github.com/travist/jsencrypt
 */
import CryptoJS from 'crypto-js'
import { JSEncrypt } from 'jsencrypt'

export const aesKey = 'KDMEUXBXJVUNJEDB'
export const iv = '1a54fc2dfda5c4fd'
/**
 * 随机生成16位的AES密钥
 */
export const getKeyAES = () => {
  const key = []
  for (let i = 0; i < 16; i++) {
    const num = Math.floor(Math.random() * 26)
    const charStr = String.fromCharCode(97 + num) // 从unicode码10进制的97开始，到122结束，随机取一个小写字母
    key.push(charStr.toUpperCase()) // 字符串转大写
  }
  const result = key.join('')
  return result
}

/**
 * MD5加密
 * 加盐处理，通过js随机生成16位字符串作为MD5加盐，将盐与加密数据拼接，再进行MD5加密
 * MD5加密一般不进行解密，这里使用MD5加密主要用来做其它加密方式的密钥或者初始向量使用，如AES加密，RSA加密等，使用MD5加密后的字符串长度为32位，如果需要16位字符串，则需要对MD5加密后的字符串进行截取
 * @param data 需要解密的数据
 * @param slat 盐
 * @param bit 生成多少位的密钥，密钥一般为16位，或者16位的倍数位，取值16/32
 * @returns 加密后的数据
 */
export const md5 = (data, slat, bit = 32) => {
  return bit === 16
    ? CryptoJS.MD5(data + slat)
        .toString()
        .substring(8, 24)
    : CryptoJS.MD5(data + slat).toString()
}

/**
 * AES加密
 * 转Utf8编码: CryptoJS.enc.Utf8.parse();
 * 转Base64: CryptoJS.enc.Base64.stringify();
 * @param data 需要加密的数据
 * @param key 密钥
 * @param iv 初始向量 一般与密钥结合使用，通常与密钥长度一致，一般在使用过程中会要求它是随机数，让攻击者难以对原文一致且使用同一把密钥生成的密文进行破解
 * @returns 加密后的数据
 */
export const encodeAES = (data, key, iv) => {
  if (typeof data !== 'string') {
    data = JSON.stringify(data)
  }
  // AES加密
  const result = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(data), CryptoJS.enc.Utf8.parse(key), {
    iv: CryptoJS.enc.Utf8.parse(iv), // 初始向量。使用CBC模式时，需要使用向量。
    mode: CryptoJS.mode.CBC, // 加密模式：CBC，CFB，CTR，CTRGladman，ECB，OFB
    padding: CryptoJS.pad.Pkcs7 // 填充方式。块密码只能对确定长度的数据块进行处理，而消息的长度通常是可变的。因此部分模式最后一块数据在加密前需要进行填充。使用非补码方式时，需要使用ZeroPadding。默认为PKCS5Padding。填充方式有：ZeroPadding，NoPadding，AnsiX923，Iso10126，Iso97971，Pkcs7。
  })
  // base64转码
  return result.ciphertext.toString().toUpperCase()
}

/**
 * AES解密
 * @param data 需要解密的数据
 * @param key 密钥
 * @param iv 初始向量 一般与密钥结合使用，通常与密钥长度一致，一般在使用过程中会要求它是随机数，让攻击者难以对原文一致且使用同一把密钥生成的密文进行破解
 * @returns 解密后的数据
 */
export const decodeAES = (data, key, iv) => {
  if (typeof data !== 'string') {
    data = JSON.stringify(data)
  }
  const result = CryptoJS.AES.decrypt(CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(data)), CryptoJS.enc.Utf8.parse(key), {
    iv: CryptoJS.enc.Utf8.parse(iv), // 初始向量。使用CBC模式时，需要使用向量。
    mode: CryptoJS.mode.CBC, // 加密模式：CBC，CFB，CTR，CTRGladman，ECB，OFB
    padding: CryptoJS.pad.Pkcs7 // 填充方式。块密码只能对确定长度的数据块进行处理，而消息的长度通常是可变的。因此部分模式最后一块数据在加密前需要进行填充。使用非补码方式时，需要使用ZeroPadding。默认为PKCS5Padding。填充方式有：ZeroPadding，NoPadding，AnsiX923，Iso10126，Iso97971，Pkcs7。
  })
  // 转为utf-8编码
  return result.toString(CryptoJS.enc.Utf8).toString()
}

/**
 * 获取RSA密钥对，公钥和私钥
 * @param func 自定义回调函数，用来接收密钥对
 * 该方法中window.crypto相关的操作，都仅支持https、localhost、127.0.0.1这三种形式的授信地址，其它形式的地址不支持，否则会报错，而且在支持的地址下，window.crypto的相关操作也还会有浏览器兼容性问题存在，不过chrome内核基本都能支持。
 */
export const getRsaKeys = (func) => {
  // var crypto = window.crypto || window.webkitCrypto
  // 			|| window.mozCrypto || window.oCrypto || window.msCrypto;
  window.crypto.subtle
    .generateKey(
      {
        name: 'RSA-OAEP',
        modulusLength: 2048, // can be 1024, 2048, or 4096
        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
        hash: { name: 'SHA-512' } // can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
      },
      true, // whether the key is extractable (i.e. can be used in exportKey)
      ['encrypt', 'decrypt'] // must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"]
    )
    .then(function (key) {
      window.crypto.subtle
        .exportKey('pkcs8', key.privateKey)
        .then(function (keydata1) {
          window.crypto.subtle
            .exportKey('spki', key.publicKey)
            .then(function (keydata2) {
              const privateKey = RSA2text(keydata1, 1)
              const publicKey = RSA2text(keydata2)
              func(privateKey, publicKey)
            })
            .catch(function (err) {
              console.error(err)
            })
        })
        .catch(function (err) {
          console.error(err)
        })
    })
    .catch(function (err) {
      console.error(err)
    })
}

/*
 * RSA密钥转文本
 */
const RSA2text = (buffer, isPrivate = 0) => {
  let binary = ''
  const bytes = new Uint8Array(buffer)
  const len = bytes.byteLength
  for (let i = 0; i < len; i++) {
    binary += String.fromCharCode(bytes[i])
  }
  const base64 = window.btoa(binary)
  let text = '-----BEGIN ' + (isPrivate ? 'PRIVATE' : 'PUBLIC') + ' KEY-----\n'
  text += base64.replace(/[^\x00-\xff]/g, '$&\x01').replace(/.{64}\x01?/g, '$&\n')
  text += '\n-----END ' + (isPrivate ? 'PRIVATE' : 'PUBLIC') + ' KEY-----'
  return text
}

/**
 * RSA加密
 * @param data 需要加密的数据
 * @param publicKey 公钥
 * @returns 加密后的数据
 */
export const encodeRSA = (data, publicKey) => {
  const encryptTool = new JSEncrypt()
  encryptTool.setPublicKey(publicKey)
  return encryptTool.encrypt(data)
}

/**
 * RSA解密
 * @param data 需要解密的数据
 * @param privateKey 私钥
 * @returns 解密后的数据
 */
export const decodeRSA = (data, privateKey) => {
  const encryptTool = new JSEncrypt()
  encryptTool.setPrivateKey(privateKey)
  return encryptTool.decrypt(data)
}

/**
 * 签名,支持SHA256签名与SHA1签名
 * @param data 需要签名的数据
 */
const signature = (data) => {
  let params = ''
  // 先对data排序，然后拼接字符串
  Object.keys(data)
    .sort()
    .forEach((item) => {
      params += `${item}=${data[item]}, `
    })
  // 去掉最后一个逗号
  params = params.slice(0, -2)
  // 以上签名规则为前后端约定好，后端接收到前端加密数据后，通过解密获取到签名和原始数据，然后用同样的规则对原始数据进行签名，如果签名一致，则说明数据是有效的，无篡改的。
  // SHA256签名：使用CryptoJS.SHA256(),先将SHA256加密,然后转Hex的16进制
  return CryptoJS.SHA256(`{${params}}`).toString(CryptoJS.enc.Hex)
}

/**
 * 为Date对象添加format方法
 */
class MyDate extends Date {
  // eslint-disable-next-line no-useless-constructor
  constructor() {
    super()
  }
  format(fmt) {
    const o = {
      'M+': this.getMonth() + 1, // 月份
      'd+': this.getDate(), // 日
      'h+': this.getHours(), // 小时
      'm+': this.getMinutes(), // 分
      's+': this.getSeconds(), // 秒
      'q+': Math.floor((this.getMonth() + 3) / 3), // 季度
      S: this.getMilliseconds() // 毫秒
    }
    if (/(y+)/.test(fmt)) {
      fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length))
    }
    for (const k in o) {
      if (new RegExp('(' + k + ')').test(fmt)) {
        fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
      }
    }
    return fmt
  }
}
/**
 * 格式化日期
 * @param date 日期
 * @param formatType 格式化类型
 */
const format = (date, formatType = 'yyyy-MM-dd hh:mm:ss') => {
  if (!date || !date.format) {
    return date
  }
  return date.format(formatType)
}

// result 类型定义
export type resultType = {
  secret?: string
  encodeKey: string | false
  requestDataEnc: string
  timestamp: string
  sign?: string
}
// 加解密工具
const Tool = {
  secret: '', // 签名用私钥，用于防止别人冒充签名，前后端约定好该私钥的生成规则
  iv: '', // 加密向量
  RSA_PUBLIC_KEY: '', // RSA公钥，后端生成RSA密钥对，公钥给前端，私钥后端自己保存
  AES_KEY: getKeyAES(), // AES密钥，在前端生成
  publicParams: {
    // clientId: 'test', // 客户端ID
  },
  /**
   * 加密
   * @param data 接口请求参数
   * 说明：请求参数data + 公共参数、AES加密、RSA加密、SHA签名
   * 另外密码等敏感参数, 需要提前单独加密
   */
  encode(data) {
    const { secret, iv, AES_KEY, RSA_PUBLIC_KEY } = this
    const requestDataEnc = encodeAES(data, AES_KEY, iv) // 所有请求参数进行AES加密
    const encodeKey = encodeRSA(AES_KEY, RSA_PUBLIC_KEY) // AES的密钥进行RSA加密
    const timestamp = format(new MyDate(), 'yyyyMMddhhmmss') // 当前时间戳
    const result: resultType = {
      secret,
      encodeKey,
      requestDataEnc,
      timestamp
    }
    // 签名
    result.sign = signature(result)
    // 防止私钥泄露移除。
    delete result.secret
    return result
  },
  /**
   * 解密
   * @param data 加密后的数据
   * @param privateKey RSA私钥
   * 该形式加密数据一般无需前端解密，前端解密私钥就必须保存在前端文件内，容易有私钥泄露的风险，但如果本身就是用此种方式加密解密前端数据，无后端参与，私钥公钥就需要先生成保存在前端文件内。
   */
  decode(data, privateKey) {
    const aesKey = decodeRSA(data.encodeKey, privateKey)
    const iv = md5(aesKey, '', 16) // 初始向量的生成方式需要与加密时的生成方式一致
    let result = decodeAES(data.requestDataEnc, aesKey, iv)
    result = JSON.parse(result)
    return result
  },
  // 加密密码
  encodePwd(data) {
    const { RSA_PUBLIC_KEY } = this
    return encodeRSA(data, RSA_PUBLIC_KEY)
  }
}

export default Tool
